/*
 * Zed Attack Proxy (ZAP) and its related class files.
 * 
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 * 
 * Copyright 2010 psiinon@gmail.com
 * Copyright 2014 Jay Ball - Aspect Security
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0 
 *   
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
package org.zaproxy.zap.extension.globalexcludeurl;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.configuration.ConversionException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.log4j.Logger;
import org.parosproxy.paros.common.AbstractParam;
import org.parosproxy.paros.model.Model;
import org.zaproxy.zap.extension.api.ZapApiIgnore;

public class GlobalExcludeURLParam extends AbstractParam {

    private static final Logger logger = Logger.getLogger(GlobalExcludeURLParam.class);

    private static final String GLOBAL_EXCLUDE_URL_BASE_KEY = "globalexcludeurl";
    
    private static final String ALL_TOKENS_KEY = GLOBAL_EXCLUDE_URL_BASE_KEY + ".url_list.url";
    
    private static final String TOKEN_REGEX_KEY = "regex";
    private static final String TOKEN_DESCRIPTION_KEY = "description";
    private static final String TOKEN_ENABLED_KEY = "enabled";

    private static final String CONFIRM_REMOVE_TOKEN_KEY = GLOBAL_EXCLUDE_URL_BASE_KEY + ".confirmRemoveToken";
    
    private static ArrayList<GlobalExcludeURLParamToken> defaultList = new ArrayList<GlobalExcludeURLParamToken>();
    
    /** Fills in the list of default regexs to ignore.  In a future version, this could be read from a
     * system-wide default HierarchicalConfiguration xml config file
     * instead or even a HierarchicalConfiguration string directly embedded in this file. */
    
    private void setDefaultList() {
        // Remember, these are regexs, so escape properly \\ vs \
        // Also, http://regexpal.com/ for quick testing.
        // The file formats are common types, not inclusive of all types. Remember, more == slower;
        // complex == slower. Don't overload with every obscure image/audio/video format in existence.
    	
    	/* At some point in the future, this could be read from some a config file and
    	 * parsed.  Thus, we just make it as arrays of strings and assume there
    	 * is some level of parsing at some point.  Since it is rarely accessed (like
    	 * once per boot, it need not be optimized).  */
    	final String defaultListArray[][] = {
			{
	    		"^.*\\.(gif|jpe?g|png|ico|icns|bmp)$",
	    	    "Extension - Image (ends with .extension)",
	            "false"
	  	    }, {
				"^.*\\.(mp[34]|mpe?g|m4[ap]|aac|avi|mov|wmv|og[gav])$",
				"Extension - Audio/Video (ends with .extension)",
				"false"
			}, {
				"^.*\\.(pdf|docx?|xlsx?|pptx?)$",
				"Extension - PDF & Office (ends with .extension)",
				"false"
			}, {
				"^.*\\.(css|js)$",
				"Extension - Stylesheet, JavaScript (ends with .extension)",
				"false"
			}, {
				"^.*\\.(sw[fa]|flv)$",
				"Extension - Flash & related (ends with .extension)",
				"false"
			}, {
				"^[^\\?]*\\.(gif|jpe?g|png|ico|icns|bmp)\\?.*$",
				"ExtParam - Image (extension plus ?params=values)",
				"false"
			}, {
				"^[^\\?]*\\.(mp[34]|mpe?g|m4[ap]|aac|avi|mov|wmv|og[gav])\\?.*$",
				"ExtParam - Audio/Video (extension plus ?params=values)",
				"false"
			}, {
				"^[^\\?]*\\.(pdf|docx?|xlsx?|pptx?)\\?.*$",
				"ExtParam - PDF & Office (extension plus ?params=values)",
				"false"
			}, {
				"^[^\\?]*\\.(css|js)\\?.*$",
				"ExtParam - Stylesheet, JavaScript (extension plus ?params=values)",
				"false"
			}, {
				"^[^\\?]*\\.(sw[fa]|flv)\\?.*$",
				"ExtParam - Flash & related (extension plus ?params=values)",
				"false"
			}, {
				"^[^\\?]*/(WebResource|ScriptResource)\\.axd\\?d=.*$",
				"ExtParam - .NET adx resources (SR/WR.adx?d=)",
				"false"
			}, {
				"^https?://api\\.bing\\.com/qsml\\.aspx?query=.*$",
				"Site - Bing API queries",
				"false"
			}, {
				"^https?://(safebrowsing-cache|sb-ssl|sb|safebrowsing\\.clients)\\.google\\.com/.*$",
				"Site - Google malware detector updates",
				"false"
			}, {
				"^https?://([^/])*\\.?lastpass\\.com",
				"Site - Lastpass manager",
				"false"
			}, {
				"^https?://(.*addons|aus[0-9])\\.mozilla\\.(org|net|com)/.*$",
				"Site - Mozilla Firefox browser updates",
				"false"
			}, {
				"^https?://([^/])*\\.?(getfoxyproxy\\.org|getfirebug\\.com|noscript\\.net)",
				"Site - Mozilla Firefox extensions phoning home",
				"false"
			}, {
				// some of this from http://serverfault.com/questions/332003/what-urls-must-be-in-ies-trusted-sites-list-to-allow-windows-update
				"^https?://(.*update\\.microsoft|.*\\.windowsupdate)\\.com/.*$",
				"Site - Microsoft Windows updates",
				"false"
			}, {
				"^https?://clients2\\.google\\.com/service/update2/crx.*$",
				"Site - Google Chrome extension updates",
				"false"
			}, {
				"^https?://detectportal\\.firefox\\.com.*$",
				"Site - Firefox captive portal detection",
				"false"
			}, {
				"^https?://www\\.google-analytics\\.com.*$",
				"Site - Google Analytics",
				"false"
			}, {
				"^https?://ciscobinary\\.openh264\\.org.*$",
				"Site - Firefox h264 codec download", // https://support.mozilla.org/t5/Firefox/Where-is-a-check-that-http-ciscobinary-openh264-org-openh264-is/m-p/1316497#M1005892
				"false"
			}, {
				"^https?://fonts.*$",
				"Site - Fonts CDNs such as fonts.gstatic.com, etc",
				"false"
			}
    	};
    	
    	for (String row[] : defaultListArray) {
    		boolean b = row[2].equalsIgnoreCase("true") ? true : false;
        	defaultList.add( new GlobalExcludeURLParamToken( row[0], row[1], b));
    	}
    }
 
    private List<GlobalExcludeURLParamToken> tokens = null;
    private List<String> enabledTokensNames = null;
    
    private boolean confirmRemoveToken = true;

    public GlobalExcludeURLParam() {
    	super();
    	setDefaultList();
    }

    @Override
    protected void parse() {
        try {
            List<HierarchicalConfiguration> fields = ((HierarchicalConfiguration) getConfig()).configurationsAt(ALL_TOKENS_KEY);
            this.tokens = new ArrayList<>(fields.size());
            enabledTokensNames = new ArrayList<>(fields.size());
            List<String> tempTokensNames = new ArrayList<>(fields.size());
            for (HierarchicalConfiguration sub : fields) {
                String regex = sub.getString(TOKEN_REGEX_KEY, "");
                if (!"".equals(regex) && !tempTokensNames.contains(regex)) {
                    boolean enabled = sub.getBoolean(TOKEN_ENABLED_KEY, true);
                    String desc = sub.getString(TOKEN_DESCRIPTION_KEY, "");
                    this.tokens.add(new GlobalExcludeURLParamToken(regex, desc, enabled));
                    tempTokensNames.add(regex);
                    if (enabled) {
                        enabledTokensNames.add(regex);
                    }
                }
            }
        } catch (ConversionException e) {
            logger.error("Error while loading Global Exclude URL tokens: " + e.getMessage(), e);
            this.tokens = new ArrayList<>(defaultList.size());
            this.enabledTokensNames = new ArrayList<>(defaultList.size());
        }
        
        if (this.tokens.size() == 0) {
            for (GlobalExcludeURLParamToken geu : defaultList) {
                this.tokens.add(new GlobalExcludeURLParamToken(geu));
            }
        }

        this.confirmRemoveToken = getBoolean(CONFIRM_REMOVE_TOKEN_KEY, true);
    }

    public List<GlobalExcludeURLParamToken> getTokens() {
        return tokens;
    }

    public void setTokens(List<GlobalExcludeURLParamToken> tokens) {
        this.tokens = new ArrayList<>(tokens);
        
        ((HierarchicalConfiguration) getConfig()).clearTree(ALL_TOKENS_KEY);

        ArrayList<String> enabledTokens = new ArrayList<>(tokens.size());
        for (int i = 0, size = tokens.size(); i < size; ++i) {
            String elementBaseKey = ALL_TOKENS_KEY + "(" + i + ").";
            GlobalExcludeURLParamToken token = tokens.get(i);
            
            getConfig().setProperty(elementBaseKey + TOKEN_REGEX_KEY, token.getRegex());
            getConfig().setProperty(elementBaseKey + TOKEN_DESCRIPTION_KEY, token.getDescription());
            getConfig().setProperty(elementBaseKey + TOKEN_ENABLED_KEY, Boolean.valueOf(token.isEnabled()));
            
            if (token.isEnabled()) {
                enabledTokens.add(token.getRegex());
            }
        }
        
        enabledTokens.trimToSize();
        this.enabledTokensNames = enabledTokens;

        // after saving, force the proxy to refresh the URL lists.
        Model.getSingleton().getSession().forceGlobalExcludeURLRefresh();
    }

    public void addToken(String regex) {
        this.tokens.add(new GlobalExcludeURLParamToken(regex));
        
        this.enabledTokensNames.add(regex);
    }

    public void removeToken(String regex) {
        this.tokens.remove(new GlobalExcludeURLParamToken(regex));
        
        this.enabledTokensNames.remove(regex);
    }

    public List<String> getTokensNames() {
        return enabledTokensNames;
    }
    
    @ZapApiIgnore
    public boolean isConfirmRemoveToken() {
        return this.confirmRemoveToken;
    }
    
    @ZapApiIgnore
    public void setConfirmRemoveToken(boolean confirmRemove) {
        this.confirmRemoveToken = confirmRemove;
        getConfig().setProperty(CONFIRM_REMOVE_TOKEN_KEY, Boolean.valueOf(confirmRemoveToken));
    }

}
